package ch.gibb.learnit.client.mapping;

import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.awt.image.BufferedImage;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;

/**
 * This class loads controller and view out of the xml, executes the controller, writes the results in the view and displays the page
 *
 * @author Bernard Jaquet
 * @version 1.0
 */
public class WebPageMapper extends HttpServlet {

    /**
     * Default path of the views
     */
    private static final String VIEWS_PATH = "/WEB-INF/classes/views/";

    /**
     * Path of the mapping xml
     */
    private static final String WEB_MAP_PATH = "/WEB-INF/classes/web-app.xml";

    /**
     * Array of supported images
     */
    private static final List<String> IMAGES = Arrays.asList("jpg", "png");

    /**
     * Instance of the logger
     */
    private final Logger masterLogger = Logger.getLogger(WebPageMapper.class);

    /**
     * The base of the URL from the web pages name.
     */
    public static final String BASE_URL = "/LearnIT/";

    /**
     * Code thrown by the the controllers if there is a redirect
     */
    public static final Integer REDIRECT_CODE = -1;

    /**
     * Gets the URL from the Request, gets the WebPageBean, sets all params, executes the logic in the controller, sets all output vars and parse the page with velocity
     *
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            masterLogger.info("Starting to get Site");
            String site = getURL(request);
            masterLogger.info("Site is " + site);
            WebPageBean page = getWebPageBeanFromURL(site);
            if (page != null) {
                displayPage(page, request, response);
                masterLogger.info("Displaying page");
                return;
            }
            ResourceBean resource = getResource(site);
            if (resource != null) {
                printResource(resource, response);
                masterLogger.info("Printing resource");
                return;
            }
            VelocityContext context = new VelocityContext();
            context.put("baseUrl", BASE_URL);
            response.getWriter().write(prepareTemplates("learnItOff", context, "404.vm"));
        } catch (Exception e) {
            response.getWriter().write(generateErrorPage(e));
            masterLogger.error("Failed to display page because " + e.getMessage(), e);
        }
    }

    /**
     * Executes the doGet()
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

    /**
     * Returns the name of the requested site without the previous parts of the Url
     *
     * @param request
     * @return
     */
    private String getURL(HttpServletRequest request) throws Exception {
        masterLogger.debug("Getting URL");
        StringBuffer url = request.getRequestURL();
        String[] urlParts = url.toString().split(BASE_URL);
        String site = (urlParts.length > 1) ? urlParts[1] : "";
        masterLogger.debug("Requested page is: " + site);
        if (site.contains("?"))
            return site.split("\\?")[0];
        return site;
    }

    /**
     * Finds the WebPageBean witch is associated to the requested Side
     *
     * @param siteName Name of the page
     * @return The requested Bean, is null if not found
     * @throws Exception Will be thrown if the web-app.xml could not be opened or parsed in getAllPages()
     */
    private WebPageBean getWebPageBeanFromURL(String siteName) throws Exception {
        masterLogger.debug("Start getting WebPageBean");
        WebPageBean temp = new WebPageBean();
        temp.setUrl(siteName);
        List<WebPageBean> pages = getAllPages();
        if (!pages.contains(temp))
            return null;
        masterLogger.debug("Page found: " + pages.get(pages.indexOf(temp)));
        return pages.get(pages.indexOf(temp));
    }

    /**
     * Parses every single Page from the web-app.xml to a WebPageBean
     *
     * @return A list of WebPageBeans
     * @throws Exception Will be thrown if the web-app.xml could not be loaded or not be parsed
     */
    private List<WebPageBean> getAllPages() throws Exception {
        masterLogger.debug("Getting all Pages");
        List<WebPageBean> webPages = new ArrayList<WebPageBean>();
        Document doc = getWebAppXml();
        NodeList pageContainer = doc.getElementsByTagName("pages");
        if (pageContainer.getLength() < 1)
            throw new Exception("Invalid web-app.xml: Only one pages element is allowed");
        else if (pageContainer.getLength() == 0)
            return webPages;
        NodeList pages = ((Element) pageContainer.item(0)).getElementsByTagName("page");
        for (int i = 0; i < pages.getLength(); i++) {
            try {
                webPages.add(parseXmlToWebPageBean((Element) pages.item(i)));
            } catch (Exception e) {
                masterLogger.error("Could not get page " + pages.item(i) + " because " + e.getMessage(), e);
            }
        }
        masterLogger.debug("Got all pages");
        return webPages;
    }

    /**
     * Creates an  WebPageBean from the given xml element page.
     *
     * @param element Must be a page element
     * @return
     * @throws Exception
     */
    private WebPageBean parseXmlToWebPageBean(Element element) throws Exception {
        masterLogger.debug("xml element is: " + element);
        WebPageBean page = new WebPageBean();
        page.setId(element.getAttribute("id"));
        page.setUrl(element.getAttribute("url"));
        page.setTemplate(element.getAttribute("template"));
        Class actionClass = Class.forName(element.getElementsByTagName("controller").item(0).getTextContent());
        page.setAction((ActionInterface) actionClass.newInstance());
        Element views = (Element) element.getElementsByTagName("views").item(0);
        page.setViewPaths(getViews(page, views.getElementsByTagName("view")));
        masterLogger.debug("Generated page " + page);
        return page;
    }

    /**
     * Creates a map of all views and their exitcodes from the xml element views
     *
     * @param page
     * @param viewList Must be a views element
     * @return
     */
    private Map<Integer, String> getViews(WebPageBean page, NodeList viewList) {
        Map<Integer, String> viewsMap = new HashMap<Integer, String>();
        for (int j = 0; j < viewList.getLength(); j++) {
            Element view = (Element) viewList.item(j);
            try {
                viewsMap.put(Integer.valueOf(view.getAttribute("exitcode")), view.getTextContent());
            } catch (NumberFormatException e) {
                masterLogger.error("Invalid web-app.xml: Invalid exticode on page " + page.getId(), e);
            }
        }
        masterLogger.debug("Got views" + viewsMap);
        return viewsMap;
    }


    /**
     * Returns the wep-app.xml as document
     *
     * @return
     * @throws Exception
     */
    private Document getWebAppXml() throws Exception {
        masterLogger.debug("Try to get web-app.xml");
        URL url = getServletContext().getResource(WEB_MAP_PATH);
        InputStream is = url.openStream();
        masterLogger.debug("web-app.xml path is: " + url.getPath());
        if (null == is)
            throw new Exception("InputStream is null");
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        Document document = docBuilder.parse(is);
        masterLogger.debug("Got web-app.xml");
        return document;
    }

    /**
     * Loads all parameters, execute the logic, get all vars with getters and generated the content of the page
     *
     * @param page WebPageBean which need to be displayed
     * @return Content of the Page
     * @throws Exception Will be thrown if something with the Veolcity went wrong or an uncatched error in the controller ocured
     */
    private void displayPage(WebPageBean page, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            setParametersFromRequest(request, page.getAction());
            page.getAction().setSession(request.getSession(true));
            page.getAction().setResponse(response);
            Integer exitCode = executePageLogic(page.getAction());
            if (exitCode == REDIRECT_CODE)
                return;
            VelocityContext context = new VelocityContext();
            assignOutputVariables(page.getAction(), context);
            response.getWriter().write(prepareTemplates(page.getTemplate(), context, page.getViewPaths().get(exitCode)));
        } catch (Exception e) {
            String msg = "Could not generate content of WebPage because " + e.getMessage();
            masterLogger.error(msg, e);
            throw new Exception(msg, e);
        }
    }

    /**
     * Creates the content of a page with a template if set
     *
     * @param templatePath Path of the template - can be null
     * @param context      Context with the variables which has to be merged
     * @param viewPath     Path of the view
     * @return
     * @throws Exception
     */
    private String prepareTemplates(String templatePath, VelocityContext context, String viewPath) throws Exception {
        VelocityEngine velocityEngine = getVeloctiyEngine();
        if (viewPath == null)
            return (prepareTemplates(templatePath, new VelocityContext(), "404.vm"));
        masterLogger.debug("Getting view " + viewPath);
        masterLogger.info("VMfile from url is: " + getServletContext().getResource(VIEWS_PATH + "/" + viewPath).getPath());
        if (templatePath == null || templatePath.equals(""))
            return mergeTemplate(context, velocityEngine, viewPath);
        TemplateBean templateBean = getTemplate(templatePath);
        if (templateBean == null)
            throw new Exception("Invalid web-app.xml: no template with id " + templatePath + " found.");
        context.put("content", mergeTemplate(context, velocityEngine, viewPath));
        context.put("baseUrl", BASE_URL);
        return mergeTemplate(context, velocityEngine, templateBean.getPath());
    }

    /**
     * Executes the method openPage in the controller
     *
     * @param action
     * @return Exit code of the page
     * @throws Exception
     */
    private Integer executePageLogic(ActionInterface action) throws Exception {
        try {
            masterLogger.debug("Execute open page in found class");
            return action.openPage();
        } catch (Exception e) {
            String msg = "An unhanded exception occurred in the controller. Reason: " + e.getMessage();
            masterLogger.error(msg, e);
            throw new Exception(msg, e);
        }
    }

    /**
     * Merges the template with the context
     *
     * @param context
     * @param velocityEngine
     * @param templatePath
     * @return
     */
    private String mergeTemplate(VelocityContext context, VelocityEngine velocityEngine, String templatePath) {
        Template template = velocityEngine.getTemplate(templatePath);
        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        return writer.toString();
    }

    /**
     * Assigns all variables from the request to the setters of the controller
     *
     * @param request
     * @param action
     * @throws Exception
     */
    private void setParametersFromRequest(HttpServletRequest request, ActionInterface action) throws Exception {
        masterLogger.debug("Reading parameters");
        for (String parameterName : request.getParameterMap().keySet()) {
            try {
                Method m = new PropertyDescriptor(parameterName, action.getClass()).getWriteMethod();
                masterLogger.debug("Setter is " + m);
                m.invoke(action, request.getParameter(parameterName));
            } catch (IntrospectionException e) {
                masterLogger.info("No setter for parameter " + parameterName);
            } catch (IllegalArgumentException e) {
                String msg = "There is a type mismatch on parameter " + parameterName;
                masterLogger.error(msg, e);
                throw new IllegalArgumentException(msg, e);
            }
            masterLogger.debug("parameter " + parameterName + " has value " + request.getParameter(parameterName));
        }
    }

    /**
     * Assigns all variables from the getters of the controller to the velocity context
     *
     * @param action
     * @param context
     * @throws Exception
     */
    private void assignOutputVariables(ActionInterface action, VelocityContext context) throws Exception {
        for (PropertyDescriptor pd : Introspector.getBeanInfo(action.getClass()).getPropertyDescriptors()) {
            if (pd.getReadMethod() != null && !"class".equals(pd.getName())) {
                context.put(pd.getName(), pd.getReadMethod().invoke(action));
                masterLogger.debug("Method added: " + pd.getName());
            }
        }
        context.put("baseUrl", BASE_URL);
    }

    /**
     * Generates a VelocityEngine. Sets different logger and ResourceLoader  as well as the encoding
     *
     * @return Parametrised engine
     */
    private VelocityEngine getVeloctiyEngine() {
        masterLogger.debug("Generating VelocityEngine");
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty("runtime.log", "/var/log/velocity/velocity.log");
        ve.setProperty("resource.loader", "mine");
        ve.setProperty("mine.resource.loader.instance", new ResourceLoaderImp(this));
        ve.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
        ve.setProperty(Velocity.INPUT_ENCODING, "UTF-8");
        ve.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");
        return ve;
    }

    /**
     * Generates the URL to access the vm-Files.
     *
     * @param name Name of the vm-File
     * @return
     * @throws MalformedURLException
     */
    public URL getServerURL(String name) throws MalformedURLException {
        masterLogger.debug("Generating URl for view" + name);
        return getServletContext().getResource(VIEWS_PATH + name);
    }

    /**
     * Generates an Page to display the occurred error
     *
     * @param t
     * @return Content of the error page
     */
    private String generateErrorPage(Throwable t) {
        masterLogger.info("Generating error page");
        StringBuilder sb = new StringBuilder("<html><head></head><body><h1>An unhanded Error occurred</h1><br />");
        sb.append("<table><tr><td valign=\"top\" >Message: </td><td>");
        sb.append(t.getMessage());
        sb.append("</td></tr><tr><td valign=\"top\" >Cause: </td><td>");
        sb.append(t.getCause());
        sb.append("</td></tr><tr><td valign=\"top\" >Stack Trace: </td><td>");
        for (StackTraceElement element : t.getStackTrace()) {
            sb.append(element.toString());
            sb.append("<br />");
        }
        sb.append("</td></tr></table></body></html>");
        return sb.toString();
    }

    /**
     * Gets a requested resource from the web-app.xml
     *
     * @param name Is the url of the resource
     * @return
     * @throws Exception
     */
    private ResourceBean getResource(String name) throws Exception {
        for (ResourceBean pageBean : getAllResources()) {
            if (pageBean.getUrl().equals(name))
                return pageBean;
        }
        return null;
    }

    /**
     * Gets the template from the web-app.xml
     *
     * @param id
     * @return
     * @throws Exception
     */

    private TemplateBean getTemplate(String id) throws Exception {
        TemplateBean tmp = new TemplateBean();
        tmp.setId(id);
        List<TemplateBean> allTemplates = getAllTemplates();
        if (allTemplates.contains(tmp))
            return allTemplates.get(allTemplates.indexOf(tmp));
        return null;
    }

    /**
     * Generates a list of all resources from the web-app.xml
     *
     * @return
     * @throws Exception
     */
    private List<ResourceBean> getAllResources() throws Exception {
        List<ResourceBean> resources = new ArrayList<ResourceBean>();
        Document doc = getWebAppXml();
        NodeList resourceContainer = doc.getElementsByTagName("resources");
        if (resourceContainer.getLength() != 1)
            throw new Exception("Invalid web-app.xml: Only one resources tag is allowed.");
        NodeList allResources = ((Element) resourceContainer.item(0)).getElementsByTagName("resource");
        for (int i = 0; i < allResources.getLength(); i++) {
            try {
                ResourceBean resource = new ResourceBean();
                Element element = (Element) allResources.item(i);
                resource.setId(element.getAttribute("id"));
                resource.setType(element.getAttribute("type"));
                resource.setUrl(element.getAttribute("url"));
                resource.setPath(element.getTextContent());
                resources.add(resource);
                masterLogger.debug("Resource added: " + resource);
            } catch (Exception e) {
                masterLogger.error("Could not get resource " + allResources.item(i) + " because " + e.getMessage(), e);
            }
        }
        return resources;
    }

    /**
     * Generates a List of all templates in the web-app.xml
     *
     * @return
     * @throws Exception
     */
    private List<TemplateBean> getAllTemplates() throws Exception {
        List<TemplateBean> templates = new ArrayList<TemplateBean>();
        Document doc = getWebAppXml();
        NodeList templateContainer = doc.getElementsByTagName("templates");
        if (templateContainer.getLength() != 1)
            throw new Exception("Invalid web-app.xml: Only one templates tag is allowed.");
        NodeList templateList = ((Element) templateContainer.item(0)).getElementsByTagName("template");
        for (int i = 0; i < templateList.getLength(); i++) {
            TemplateBean t = new TemplateBean();
            try {
                Element e = (Element) templateList.item(i);
                t.setId(e.getAttribute("id"));
                t.setPath(e.getTextContent());
                templates.add(t);
                masterLogger.debug("Template added: " + t);
            } catch (Exception e) {
                masterLogger.error("Could not get template " + t + "because " + e.getMessage(), e);
            }
        }
        return templates;
    }

    /**
     * Displays a resource on the web page
     *
     * @param resource
     * @param response
     * @throws IOException
     */
    private void printResource(ResourceBean resource, HttpServletResponse response) throws IOException {
        URL url = getServerURL(resource.getPath());
        if (IMAGES.contains(resource.getType())) {
            displayImage(resource, response, url);
        } else {
            URLConnection connection = getServerURL(resource.getPath()).openConnection();
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            StringBuilder responseText = new StringBuilder();
            String inputLine;
            while ((inputLine = in.readLine()) != null)
                responseText.append(inputLine).append("\n");
            in.close();
            response.getWriter().write(responseText.toString());
        }
    }

    /**
     * This is to display images because they need a special treatment
     *
     * @param resource
     * @param response
     * @param url
     * @throws IOException
     */
    private void displayImage(ResourceBean resource, HttpServletResponse response, URL url) throws IOException {
        masterLogger.debug("Resource is a pic, special treatment");
        response.reset();
        response.setContentType("image/" + resource.getType());
        response.setHeader("Content-disposition", "attachment; filename=\"" + resource.getId() + "\"");
        BufferedImage img = ImageIO.read(url);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(img, resource.getType(), baos);
        baos.flush();
        byte[] imgByte = baos.toByteArray();
        baos.close();
        OutputStream os = response.getOutputStream();
        os.write(imgByte);
        os.close();
    }
}
